<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>函数扩展</title>
</head>
<body>
<script type="text/javascript">
    {
        /**函数参数的默认值 */
        function log(x, y = 'World') {
            console.log(x, y);
        }
        log('Hello'); // Hello World
        log('Hello', 'China'); // Hello China
        log('Hello', ''); // Hello World

        class Point {
            constructor(x = 0, y = 0) {
                this.x = x;
                this.y = y;
            }
        }
        var p = new Point();
        console.log(p); // { x: 0, y: 0 }
    }

    {
        /** 与解构赋值默认值结合使用 */
        function foo({ x, y = 5 }) {
            console.log(x, y);
        }
        foo({}); // undefined, 5
        foo({ x: 1 }); // 1, 5
        foo({ x: 1, y: 2 }); // 1, 2
        // foo(); // TypeError: Cannot read property 'x' of undefined
    }

    {
        /** 参数默认值的位置 */
        function f(x = 1, y) {
            return [x, y];
        }
        f(); // [1, undefined]
        f(2); // [2, undefined])
        // f(, 1) // 报错
        f(undefined, 1); // [1, 1]

        // 指定了默认值以后，函数的length属性，将返回没有指定默认值的参数个数。
        console.log((function (a) { }).length); // 1
        console.log((function (a = 5) { }).length); // 0
        console.log((function (a, b, c = 5) { }).length); // 2

        // 如果设置了默认值的参数不是尾参数，那么length属性也不再计入后面的参数了。
        console.log((function (a = 0, b, c) { }).length); // 0
        console.log((function (a, b = 1, c) { }).length); // 1
    }

    {
        /** 作用域：如果参数默认值是一个变量，则该变量所处的作用域，与其他变量的作用域规则是一样的，即先是当前函数的作用域，然后才是全局作用域。*/
        let x = 1;
        function f(x, y = x) {
            console.log(x, y);
        }
        f(2); // 2,2，参数y的默认值等于x。调用时，由于函数作用域内部的变量x已经生成，所以y等于参数x，而不是全局变量x。

        let a = 1;
        function fn(b = a) {
            let a = 2;
            console.log(a, b);
        }
        fn(); // 2,1，函数调用时，y的默认值变量x尚未在函数内部生成，所以x指向全局变量。

        function f2(c = d) {
            const d = 2;
            console.log(d);
        }
        // f2(); // ReferenceError: x is not defined. 全局变量c不存在，就会报错。
    }

    {
        /**
         * rest参数
         * ES6引入rest参数（形式为“...变量名”），用于获取函数的多余参数，这样就不需要使用arguments对象了。
         * rest参数搭配的变量是一个数组，该变量将多余的参数放入数组中。
         * 注意，rest参数之后不能再有其他参数（即只能是最后一个参数），否则会报错。
         */
        function printValue(...args) {
            for (const val of args) {
                console.log(val);
            }
        }
        printValue(1, 2, 3, 4, '5'); // 1 2 3 4 5

        /* function f(a,...b,c) {
            // ... 报错
        } */
    }

    {
        /** 扩展运算符：扩展运算符（spread）是三个点（...）。它好比rest参数的逆运算，将一个数组转为用逗号分隔的参数序列。*/
        console.log(...[1, 2, 3]); // 1 2 3
        console.log(1, ...[2, 3, 4], 5); // 1 2 3 4 5

        // 替代数组的apply方法
        // 应用Math.max方法，简化求出一个数组最大元素的写法。
        // ES5写法
        console.log(Math.max.apply(null, [14, 3, 22])); // 22

        // ES6写法
        console.log(Math.max(...[14, 3, 22])); // 22
        console.log(Math.max(14, 3, 22)); // 22

        // 通过push函数，将一个数组添加到另一个数组的尾部。
        // ES5写法
        const arr1 = [0, 1, 2];
        const arr2 = [3, 4, 5];
        // push方法的参数不能是数组，所以只好通过apply方法变通使用push方法。
        console.log(Array.prototype.push.apply(arr1, arr2)); // 6

        // ES6写法
        const arr3 = [0, 1, 2];
        const arr4 = [3, 4, 5];
        console.log(arr3.push(...arr4));

        // ES5写法
        console.log(new (Date.bind.apply(Date, [null, 2019, 1, 1]))); // Fri Feb 01 2019 00:00:00 GMT+0800 (中国标准时间)

        // ES6写法
        console.log(new Date(...[2019, 1, 1]));
    }

    {
        /** 扩展运算符的应用 */
            // (1)合并数组
            // ES5写法
        const arr1 = [0, 1, 2];
        const arr2 = [3, 4, 5];
        const arr3 = [6];
        // console.log(arr1.concat(arr2, arr3)); // [0, 1, 2, 3, 4, 5, 6]
        // ES6写法
        console.log([...arr1, ...arr2, ...arr3]); // [0, 1, 2, 3, 4, 5, 6]

        // (2)与解构赋值结合，用于生成数组。
        const [first, ...rest] = [1, 2, 3, 4, 5];
        console.log(first); // 1
        console.log(rest); // [2, 3, 4, 5]

        // (3)函数的返回值：JavaScript的函数只能返回一个值，如果需要返回多个值，只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
        // var dateFields = readDateFields(database);
        // var d = new Date(...dateFields);

        // (4)字符串
        console.log([...'hello']); // [ "h", "e", "l", "l", "o" ]

        // (5)实现了Iterator接口的对象
        let arrayLike = {
            '0': 'a',
            '1': 'b',
            '2': 'c',
            length: 3
        };
        // console.log([...arrayLike]); // 报错：arrayLike is not iterable
        console.log(Array.from(arrayLike)); // ["a", "b", "c"]

        // (6)Map和Set结构，Generator函数
        let map = new Map([
            [1, 'one'],
            [2, 'two'],
            [3, 'three'],
        ]);
        console.log([...map.keys()]); // [1, 2, 3]

        let set = new Set([[1, 'one'], [2, 'two'], [3, 'three']]);
        console.log([...set.entries()]); // [1, "one"],[2, "two"],[3, "three"]

        let go = function* () {
            yield 1;
            yield 2;
            yield 3;
        };
        console.log([...go()]); // [1, 2, 3]
    }

    {
        /** 箭头函数 */
        let f = v => v + 2;
        console.log(f(3)); // 5

        // 如果箭头函数不需要参数或需要多个参数，就使用一个圆括号代表参数部分。
        let f2 = () => 5;
        console.log(f2()); // 5

        let sum = (num1, num2) => num1 + num2;
        console.log(sum(1, 3)); // 4

        // 如果箭头函数的代码块部分多于一条语句，就要使用大括号将它们括起来，并且使用return语句返回。
        // let sum2 = (num3, num4) => { return num3 * num4 };
        let sum2 = (num3, num4) => num3 * num4;
        console.log(sum2(3, 4)); // 12

        // 简化回调函数
        console.log([1, 2, 3].map(x => x * x)); // [1, 4, 9]
    }

    {
        /** 尾调用优化：指某个函数的最后一步是调用另一个函数。*/
        function tail(x) {
            console.log(x);
        }
        function func(x) {
            return tail(x);
        }
        func(233); // 233
    }

    {
        /** 尾递归：函数调用自身，称为递归。如果尾调用自身，就称为尾递归。*/
        function factorial(n) {
            if (n === 1) return 1;
            return n * factorial(n - 1);
        }
        console.log(factorial(5)); // 120，计算n的阶乘，最多需要保存n个调用记录

        // 改写成尾递归，只保留一个调用记录
        function factorial2(n, total = 1) {
            if (n === 1) {
                return total;
            }
            return factorial2(n - 1, n * total);
        }
        console.log(factorial2(5)); // 120
    }
</script>
</body>
</html>